// @vitest-environment jsdom
//
// `/blog/[slug]` — the three static-generation exports: `getStaticPaths`
// (enumerate slugs), `loader` (fetch this post, `notFound()` for a miss), and
// `meta(loaderData)` (per-post title). Plus the article render. The page reads
// `useLoaderData` and renders a `<ReadingProgress>` island — both come from
// `@voltro/web`, mocked here so `useLoaderData` returns our post and `island`
// is the identity wrapper.

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { act, createElement, type ComponentType, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { posts, type Post } from '../../../content/posts'
import en from '../../../locales/en'

const loaderData = vi.fn<() => Post>()

const fill = (msg: string, values?: Record<string, unknown>): string =>
  values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg

class NotFoundError extends Error {}
vi.mock('@voltro/web', () => ({
  useLoaderData: () => loaderData(),
  // Real `notFound()` throws to abort the loader; a distinct error lets us
  // assert the miss path without pulling in the router runtime.
  notFound: (detail?: string) => {
    throw new NotFoundError(detail)
  },
  // Real `island()` wraps the component in a hydration marker; identity keeps
  // the inner component observable in a unit test.
  island: (Component: ComponentType<unknown>) => Component,
  // `withLocalePrefix` (lib/locale) reads useLocation; the post page pins the
  // locale via useLocale, so any bare path is fine here.
  useLocation: () => '/',
}))

// The page renders chrome (back-link, reading-time) via <T>/useLocale — resolve
// them against the real English catalog with a controllable "current locale".
vi.mock('@voltro/i18n', () => ({
  defineCatalog: <T,>(c: T): T => c,
  defineLocale:
    () =>
    <T,>(c: T): T =>
      c,
  useLocale: () => 'en',
  T: ({ id, values }: { id: string; values?: Record<string, unknown> }) =>
    fill((en as Record<string, string>)[id] ?? id, values),
}))

const { default: BlogPost, getStaticPaths, loader, meta } = await import('./page')

;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true

let container: HTMLDivElement
let root: Root

const render = (node: ReactNode): void => {
  container = document.createElement('div')
  document.body.appendChild(container)
  act(() => {
    root = createRoot(container)
    root.render(node)
  })
}

beforeEach(() => {
  loaderData.mockReset()
})

afterEach(() => {
  if (root) act(() => root.unmount())
  container?.remove()
  document.body.innerHTML = ''
})

describe('blog [slug] — getStaticPaths', () => {
  test('enumerates one path per post', async () => {
    const paths = await getStaticPaths()
    expect(paths.map((p) => p.params.slug)).toEqual(posts.map((p) => p.slug))
  })
})

describe('blog [slug] — loader', () => {
  test('returns the matching post for a known slug', async () => {
    const post = await loader({ params: { slug: 'hello-static' } } as never)
    expect(post.slug).toBe('hello-static')
    expect(post.title).toContain('zero JavaScript')
  })

  test('throws notFound for an unknown slug', async () => {
    await expect(loader({ params: { slug: 'nope' } } as never)).rejects.toBeInstanceOf(
      NotFoundError,
    )
  })
})

describe('blog [slug] — meta', () => {
  test('builds a per-post title + description from the loader data', () => {
    const post = posts[0]!
    const m = meta({ loaderData: post })
    expect(m.title).toContain(post.title)
    expect(m.description).toBe(post.excerpt)
  })
})

describe('blog [slug] — render', () => {
  test('renders the post title, meta line, and each body paragraph', () => {
    const post = posts[1]!
    loaderData.mockReturnValue(post)
    render(createElement(BlogPost))
    expect(container.querySelector('article h1')?.textContent).toBe(post.title)
    expect(container.textContent).toContain(`${post.readingMinutes} min read`)
    const paras = post.body.split('\n\n')
    expect(container.querySelectorAll('article p').length).toBeGreaterThanOrEqual(paras.length)
  })
})
